<?php
/**
 * Parser components for the Nephtali Web Framework documentation generation library.
 *
 * @author Adam Richardson of Envision Internet Consulting, LLC
 * @license http://www.opensource.org/licenses/mit-license.php MIT License
 */

/**
 * General namespace for nlib doc_generator tools.
 */
namespace nlib\doc_generator;
/**
 * When true, displays debug info created throughout parsing process.
 */
const DEBUG = false;

/**
 * Filters are checks to get into a new parsing mode.
 */

const FILTER_HTML = 1;

const FILTER_PHP = 2;

const FILTER_NAMESPACE_BLOCKED = 4;

const FILTER_NAMESPACE_STANDARD = 8;

const FILTER_CLASS = 16;

const FILTER_FUNCTION = 32;

const FILTER_CONSTANT = 64;

const FILTER_STRING_SINGLE = 128;

const FILTER_STRING_DOUBLE = 256;

const FILTER_COMMENT_SINGLE = 512;

const FILTER_COMMENT_MULTI = 1024;

/**
 * Modes determine the actions carried out on the current buffer.
 */

const MODE_HTML = 16386;

const MODE_PHP = 34812;

const MODE_NAMESPACE_BLOCKED = 67568;

const MODE_NAMESPACE_STANDARD = 133104;

const MODE_CLASS = 264160;

const MODE_FUNCTION = 526208;

const MODE_CONSTANT = 1048960;

const MODE_STRING_SINGLE = 2097152;

const MODE_STRING_DOUBLE = 4194304;

const MODE_COMMENT_SINGLE = 8388608;

const MODE_COMMENT_MULTI = 16777216;

if (DEBUG) require_once 'nlib/doc_generator/dev_tools.php';

class Doc{
	public $abstract = false;
	public $author = null;
	public $deprecated = null;
	public $description = '';
	public $global = null;
	public $license = null;
	public $params = array();
	public $return = null;
	public $static = false;
}

class GlobalTag{
	public $type;
	public $name;
	public $description;
}

class ParamTag{
	public $type = null;
	public $name = null;
	public $description = null;
}

class ReturnTag{
	public $type = null;
	public $description = null;
}
/**
 * Base class for basic node structure.
 */
class Node{
	/**
	 * Stores the name of the node.
	 *
	 * @var string
	 */
	public $name;
	/**
	 * Stores the doc object for the node.
	 *
	 * @var Doc
	 */
	public $doc;
}
/**
 * Class for root node forming the document structure.
 */
class DocNode extends Node {
	/**
	 * Array of namespace object children contained within the document.
	 *
	 * @var array
	 */
	public $namespaces = array();
	/**
	 * Document to be used for the next document-eligible node.
	 *
	 * @var Doc
	 */
	public $doc_in_waiting = null;
	/**
	 * Line number the docblock ends on.
	 *
	 * @var int
	 */
	public $doc_in_waiting_closing_line = null;
	/**
	 * Total lines in PHP file documented.
	 *
	 * @var int
	 */
	public $total_lines = 1;
}

class NamespaceNode extends Node{
	public $constants = array();
	public $functions = array();
	public $classes = array();
}

class ClassNode extends Node {
	public $constants = array();
	public $properties = array();
	public $methods = array();
}

class ClassConstantNode extends Node {}

class ClassPropertyNode extends Node {}

class ClassMethodNode extends Node {}

class FunctionNode extends Node {}

class ConstantNode extends Node {}
/**
 *
 * @param array $remaining_code_chars Reverse-ordered character array of the remaining code to be parsed.
 * @param Node $node Node object that stores the results of parsing.
 * @param string $string_buffer Existing string buffer starting from character after the opening '/*' opening and ending with the closing * and /.
 * @return array Var $remaining_code_chars with '*' and '/' appended so MODE_COMMENT_MULTI can naturally exit in parse_php_file().
 */
function parse_potential_docblock(array $remaining_code_chars, DocNode $node, $string_buffer)
{
	if (DEBUG) {
		echo "====================\n";
		echo "Mode: Docblock\n\n";
		echo "Comment:\n\n\*".$string_buffer."\n\n";
		echo "Next 20 characters:\n\n".implode('', array_slice(array_reverse($remaining_code_chars), 0, 20))."\n\n";
	}

	// break up comment content by lines
	$lines = \explode("\n", $string_buffer);
	// should have at least 3 lines and start *
	if (count($lines) < 3 || $lines[0][0] != '*') {
		if (DEBUG) echo "Exited because the comment was not a docblock (it was to short or did not start with 2 asterisks.)";
		return $remaining_code_chars;
	}
	// first line should be rest of opening comment
	unset($lines[0]);
	// last line should be closing comment
	array_pop($lines);
	$doc = new Doc();
	$in_description = TRUE;

	foreach ($lines as $line) {
		$line = \trim($line);
		$matches = array();

		if (!\strlen($line) || $line[0] != '*') {
			// not a docblock
			if (DEBUG) echo "Exited because the comment was not a docblock (at least one of the lines did not begin with an asterisk.)";
			return $remaining_code_chars;
		}

		if (\strpos($line, '@') == 2) {
			// it's a tag
			$words = \explode(' ', \substr($line, 3));

			switch ($words[0]) {
				case 'abstract':
					$doc->abstract = true;
					break;
				case 'author':
					unset($words[0]);

					if ($words) {
						$doc->author = \implode(' ', $words);
					}

					break;
				case 'deprecated':
					unset($words[0]);

					if ($words) {
						$doc->deprecated = \implode(' ', $words);
					}

					break;
				case 'global':
					unset($words[0]);

					if ($words) {
						$doc->global = \implode(' ', $words);
					}

					break;
				case 'license':
					unset($words[0]);

					if ($words) {
						$doc->license = \implode(' ', $words);
					}

					break;
				case 'param':
					$param = new ParamTag();
					$param->type = $words[1];
					
					if (isset($words[2])) {
						$param->name = $words[2];
					}

					unset($words[0], $words[1], $words[2]);

					if ($words) {
						$param->description = \implode(' ', $words);
					}

					$doc->params[] = $param;
					break;
				case 'return':
					$return = new ReturnTag();
					$return->type = $words[1];
					unset($words[0], $words[1]);
					$return->description = \implode(' ', $words);
					$doc->return = $return;
					break;
				case 'static':
					$doc->static = true;
					break;
			}

		} else {
			if ($in_description) {
				if (strlen($line) > 2) {
					$doc->description .= \substr($line, 2)."\n";
				} else {
					$doc->description .= "\n";
				}
			} else {
				// shouldn't get here, not a docblock
				return $remaining_code_chars;
			}
		}
	}

	// if we make it here, it's a valid docblock
	if (is_null($node->doc)) {
		$node->doc = $doc;
	}

	$doc->description = \trim($doc->description);
	$node->doc_in_waiting = $doc;
	$node->doc_in_waiting_closing_line = $node->total_lines;
	return $remaining_code_chars;
}
/**
 * Recursive parser function.
 *
 * Don't overcomplicate the function below, as it's form is really simple.  Every mode has
 * an entry and an exit.
 *
 * There are a few reasons why the code isn't broken up into separate functions:
 * - Performance.  Do you really NEED to add all those calls added to the stack?
 * - The modes all have 2 conditional statements (unless shared), one for triggering the mode, and one for exiting, keeping a clean organization.
 * - This keeps the recursion easy(er) and the file small.
 *
 * @param array $remaining_code_chars Reverse-ordered character array of the remaining code to be parsed.
 * @param Node $node Node object that stores the results of parsing.
 * @param int $mode One of the MODE_ constants, which are bitmasks of the filters for the mode.
 * @return array Array of remaining reverse-ordered code characters.
 */
function parse_php_file(array $remaining_code_chars, DocNode $node, $mode)
{
	if (DEBUG) {
		echo "====================\n";
		echo "Mode: ".mode_name($mode)."\n";
		echo "Line #: ".current_line_number($char_array = $remaining_code_chars)."\n";
		echo "Next 20 characters:\n".implode('', array_slice(array_reverse($remaining_code_chars), 0, 20))."\n\n";
	}

	$string_buffer = '';
	$string_buffer_length = 0;

	while (count($remaining_code_chars)) {
		$matches = array();
		$char = \array_pop($remaining_code_chars);

		$string_buffer .= $char;
		$string_buffer_length = \strlen($string_buffer);

		// manually go through triggers and exits and determine set of characters that are special and only act on those
		if (($char != "'") && ($char != '"') && ($char != '*') && ($char != '/') && ($char != "\n") && ($char != '=') && ($char != ';') && ($char != '{') && ($char != '}') && ($char != '>') && ($char != 'p')) {
			continue;
		}
		// used to keep track of current line number and keep newlines from building in the buffer
		if ($char == "\n") {
			$node->total_lines = $node->total_lines + 1;
		}
		// string single trigger
		if (($mode & FILTER_STRING_SINGLE) && ($char == "'")) {
			$remaining_code_chars = parse_php_file($remaining_code_chars, $node, MODE_STRING_SINGLE);
			// clean up local buffer
			$string_buffer = \substr($string_buffer,0,-1);
			continue;
		}
		// string single exit
		if (($mode == MODE_STRING_SINGLE) && ($char == "'") && (($string_buffer_length == 1) || ($string_buffer[$string_buffer_length - 2] != '\\'))) {
			if (DEBUG) echo "Exited string (single) line #: ".current_line_number($char_array = $remaining_code_chars)."\n";
			return $remaining_code_chars;
		}
		// string double trigger
		if (($mode & FILTER_STRING_DOUBLE) && ($char == '"')) {
			$remaining_code_chars = parse_php_file($remaining_code_chars, $node, MODE_STRING_DOUBLE);
			$string_buffer = \substr($string_buffer,0,-1);
			continue;
		}
		// string double exit
		if (($mode == MODE_STRING_DOUBLE) && ($char == '"') && (($string_buffer_length == 1) || ($string_buffer[$string_buffer_length - 2] != '\\'))) {
			if (DEBUG) echo "Exited string (double) line #: ".current_line_number($char_array = $remaining_code_chars)."\n";
			return $remaining_code_chars;
		}
		// comment multi trigger
		if (($mode & FILTER_COMMENT_MULTI) && ($char == '*') && ($string_buffer_length > 1) && ($string_buffer[$string_buffer_length - 2] == '/')) {
			$remaining_code_chars = parse_php_file($remaining_code_chars, $node, MODE_COMMENT_MULTI);
			$string_buffer = \substr($string_buffer,0,-2);
			continue;
		}
		// comment multi exit
		if (($mode == MODE_COMMENT_MULTI) && ($char == '/') && ($string_buffer_length > 1) && ($string_buffer[$string_buffer_length - 2] == '*')) {
			if (DEBUG) echo "Exited comment (multi) line #: ".current_line_number($char_array = $remaining_code_chars)."\n";
			return $remaining_code_chars = parse_potential_docblock($remaining_code_chars, $node, $string_buffer);
		}
		// comment single trigger
		if (($mode & FILTER_COMMENT_SINGLE) && ($char == '/') && ($string_buffer_length > 1) && ($string_buffer[$string_buffer_length - 2] == '/')) {
			$remaining_code_chars = parse_php_file($remaining_code_chars, $node, MODE_COMMENT_SINGLE);
			$string_buffer = \substr($string_buffer,0,-2);
			continue;
		}
		// comment single exit
		if (($mode == MODE_COMMENT_SINGLE) && ($char == "\n")) {
			if (DEBUG) echo "Exited comment (single) line #: ".current_line_number($char_array = $remaining_code_chars)."\n";
			return $remaining_code_chars;
		}
		// constant trigger
		if (($mode & FILTER_CONSTANT) && ($string_buffer_length > 9) && ($char == '=') && (($pos = \strpos($string_buffer, 'const')) !== false) && (\preg_match($pattern = '/[\s]+const[\s]+([a-zA-Z_][a-zA-Z0-9_]*)[\s]*=/', $subject = $string_buffer, $matches))) {
			// create new const node and toss in info available
			$const_node = new ConstantNode();
			$const_node->name = $matches[1];
			$ns_len = \count($node->namespaces);
			$ns_current = $node->namespaces[$ns_len - 1];
			$ns_current->constants[] = $const_node;

			if (!is_null($node->doc_in_waiting) && ($node->doc_in_waiting_closing_line + 1 == ($node->total_lines - \substr_count($string_buffer, "\n", $pos)))) {
				$const_node->doc = $node->doc_in_waiting;
				$node->doc_in_waiting = null;
			}

			$remaining_code_chars = parse_php_file($remaining_code_chars, $node, MODE_CONSTANT);
			// reset local buffer
			$string_buffer = '';
			continue;
		}
		// constant exit
		if (($mode == MODE_CONSTANT) && ($char == ';')) {
			if (DEBUG) echo "Exited constant line #: ".current_line_number($char_array = $remaining_code_chars)."\n";
			return $remaining_code_chars;
		}
		// blocked namespace
		if (($mode & FILTER_NAMESPACE_BLOCKED) && ($string_buffer_length > 12) && ($char == '{') && (($pos = \strpos($string_buffer, 'namespace')) !== false) && (\preg_match($pattern = '/[\s]+namespace[\s]+([a-zA-Z_][\\a-zA-Z0-9_]*)[\s]*{/', $subject = $string_buffer, $matches))) {
			// trim matches[1] b/c regex lets whitespace sneak in
			$ns_node = new NamespaceNode();
			$ns_node->name = trim($matches[1]);
			$node->namespaces[] = $ns_node;

			if (!is_null($node->doc_in_waiting) && ($node->doc_in_waiting_closing_line + 1 == ($node->total_lines - \substr_count($string_buffer, "\n", $pos)))) {
				$ns_node->doc = $node->doc_in_waiting;
				$node->doc_in_waiting = null;
			}

			$remaining_code_chars = parse_php_file($remaining_code_chars, $node, MODE_NAMESPACE_BLOCKED);
			// reset local buffer
			$string_buffer = '';
			continue;
		}
		// function trigger
		if (($mode & FILTER_FUNCTION) && ($string_buffer_length > 13) && ($char == '{') && (($pos = \strpos($string_buffer, 'function')) !== false) && (\preg_match($pattern = '/[\s]+function[\s]+([a-zA-Z_][a-zA-Z0-9_]+)[\s]*\([^{]*\)[\s]*{/', $subject = $string_buffer, $matches))) {
			// create new function node and toss in info available
			$func_node = new FunctionNode();
			$func_node->name = $matches[1];
			$ns_len = \count($node->namespaces);

			if ($ns_len < 1) {
				$ns_current = new NamespaceNode();
				$ns_current->name = '[Global]';
				$node->namespaces[] = $ns_node;
			} else {
				$ns_current = $node->namespaces[$ns_len - 1];
			}

			$ns_current->functions[] = $func_node;

			if (!is_null($node->doc_in_waiting) && ($node->doc_in_waiting_closing_line + 1 == ($node->total_lines - \substr_count($string_buffer, "\n", $pos)))) {
				$func_node->doc = $node->doc_in_waiting;
				$node->doc_in_waiting = null;
			}

			$remaining_code_chars = parse_php_file($remaining_code_chars, $node, MODE_FUNCTION);
			// reset local buffer
			$string_buffer = '';
			continue;
		}
		// function and blocked namespace exit
		if (($mode == MODE_NAMESPACE_BLOCKED || MODE_FUNCTION == $mode) && ($char == '}')) {
			if ((\substr_count($string_buffer, '{') + 1) == \substr_count($string_buffer, '}')) {
				if (DEBUG) echo "Exited namespace or function line #: ".current_line_number($char_array = $remaining_code_chars)."\n";
				return $remaining_code_chars;
			}
		}
		// php exit
		if (($mode & FILTER_PHP) && ($char == '>') && ($string_buffer_length > 1) && ($string_buffer[$string_buffer_length -2] == '?')) {
			$remaining_code_chars = parse_php_file($remaining_code_chars, $node, MODE_HTML);
			// clean up local buffer
			$string_buffer = \substr($string_buffer,0,-2);
			if (DEBUG) echo "Exited PHP line #: ".current_line_number($char_array = $remaining_code_chars)."\n";
			continue;
		}
		// php trigger
		if (($mode == MODE_HTML) && ($char == 'p') && ($string_buffer_length > 4) && ($string_buffer[$string_buffer_length - 2] == 'h') && ($string_buffer[$string_buffer_length - 3] == 'p') && ($string_buffer[$string_buffer_length - 4] == '?') && ($string_buffer[$string_buffer_length - 5] == '<')) {
			$remaining_code_chars = parse_php_file($remaining_code_chars, $node, MODE_PHP);
			// clean up local buffer
			$string_buffer = \substr($string_buffer,0,-5);
			continue;
		}
	}

	return array();
}
/**
 * Primes the pump, so-to-speak, getting things ready to use the parse_php_file() function.
 *
 * @param string $code
 * @param Node $node
 * @return Node
 */
function start_parsing($code, DocNode $node)
{
	$remaining_code_chars = array_reverse(str_split(trim($code)));
	parse_php_file($remaining_code_chars, $node, $mode = MODE_HTML);
	return $node;
}

?>